Skip to content

fix(audio): a silent transcoding bridge names itself instead of dying as a muxer error (#396) - #397

Merged
superuser404notfound merged 1 commit into
mainfrom
fix/ae396-bridge-silent-zero-output
Aug 18, 2026
Merged

fix(audio): a silent transcoding bridge names itself instead of dying as a muxer error (#396)#397
superuser404notfound merged 1 commit into
mainfrom
fix/ae396-bridge-silent-zero-output

Conversation

@superuser404notfound

Copy link
Copy Markdown
Owner

Closes nothing yet: AE#396 stays open for the reporter's capture. This is the instrumentation and the classification that make their next run answer the question.

What the report is

A plain SD MKV with mono MP3 audio fails on the native route at nine of nine start positions, ending on Source audio cannot be muxed (code -22) after three identical revive attempts of 12 to 23 ms each.

What is actually happening

The muxer is right and innocent. FFmpeg's mp4 muxer can only build an AC-3/E-AC-3 sample entry from a packet it has PARSED, and on a bridged source those packets come from the bridge's encoder, not from the source. The bridge had emitted none, so the first cut deferred (AE#222's guard, doing its job), the pump exited muxerFailed, and the revive cap ended the session.

Reproduced 1:1 on a synthetic fixture (mono MP3 44.1 kHz MKV whose audio payloads were made undecodable): same log lines, same three attempts, same terminal string.

Nothing in the session said any of that. Every step between a source packet and an encoded frame ends in a return or in a loop that stops on a negative code:

  • avcodec_send_packet answering invalidData / einval (a deliberate per-packet skip, Blu-ray ISO playback failed, audio bridge.feed failed at pkt #64)
  • while avcodec_receive_frame(...) >= 0, which reads "drain what is there" and behaves as "drop the reason there is nothing"
  • four early returns in resampleAndPushIntoFIFO
  • avcodec_receive_packet breaking on any negative code
  • the producer ignoring writePacket's rc on the bridged arm

Per packet all of that is correct: one bad frame must not end a session. In aggregate it made a bridge that emitted nothing for a whole first segment indistinguishable from one that had simply not been asked yet, and the only sentence the session ever produced named the muxer and the source, which are the two subsystems that were fine.

What this changes

  • AudioBridge.FeedStats: fed, rejected, decoded, dropped-before-FIFO, enqueued, emitted, decode/encode errors, and the decoder's own last error code instead of discarding it. One loud line the moment enough source has gone in for the silence to be structural rather than start-up latency (64 packets, two orders past the worst-case first output).
  • The deferred cut tells the truth on the bridged path. It used to announce "scanning forward for a prime frame" while scanForAudioMoovPrimeFrame returns without looking there (it is scoped to stream-copy audio by design, a bridged session has nothing in the source to scan for). A log line describing an action that never happens, on the one path where the interesting question had no line at all. It now prints the bridge's account.
  • Zero decoded frames ends the session at once. A restart rebuilds the muxer and re-opens the encoder, both downstream of a failing decoder, so the revive budget bought the same answer three times. Frames decoded with nothing emitted is the ENCODER side, which a rebuild does heal (VOD start at resume position with bridged audio (mp3→EAC3) never becomes ready; recovery re-anchor kills the muxer ("Cannot write moov atom before EAC3 packets parsed") with no escalation (#93 residual) #99 failure mode B), and keeps its revive.
  • PlaybackErrorKind.audioBridgeProducedNoOutput. This used to arrive as .vodSourceFailed, which reads as "the source is gone" and ends a host's fallback ladder. The source is neither gone nor unreadable here, and a second player that decodes the track itself plays the file, so a ladder should DEMOTE on this kind, not stop.

Verification

Fixture A/B with aetherctl play:

  • before: three revive attempts, state=error("Source audio cannot be muxed (code -22)"), no mention of audio decoding anywhere in the session
  • after: AE#396 seg-0.m4s cut deferred: the audio sample entry is built from a BRIDGED packet and the bridge has muxed none. Bridge: fed=2 decoded=0 enqueued=0 emitted=0 rejected=2 lastDecodeError=Invalid data found when processing input (-1094995529), then state=error("Audio track could not be decoded (code -22)") immediately

Control: the same fixture undamaged plays unchanged (item.audioTrack codec='ec-3' sr=44100 ch=1, no new lines, no early exit). Long-GOP and normal-GOP variants both clean.

Issue396SilentAudioBridgeTests: 4 tests, including a control that a bridge which DID emit keeps the muxer verdict (verified to fail when the branch is made unconditional).

1936 swift-testing + 547 XCTest green, no new warnings.

🤖 Generated with Claude Code

https://claude.ai/code/session_01E5UztmCZtNYPQpzdmSdmWG

… as a muxer error (#396)

A plain SD MKV with mono MP3 audio failed on the native route at nine of nine
start positions, ending on "Source audio cannot be muxed (code -22)" after three
identical revive attempts. The muxer was right and innocent: movenc can only
build an AC-3/E-AC-3 sample entry from a packet that has been written, and on a
bridged source those packets come from the bridge's encoder, which had emitted
none.

Nothing anywhere in the session said that. Every step between a source packet
and an encoded frame ends in a `return` or in a loop that stops on a negative
code: a packet the decoder rejects, a decoder that answers nothing, a resample
that converts to zero samples, an encoder that keeps its output. Per packet that
is correct, one bad frame must not end a session. In aggregate it made a bridge
that emitted nothing for a whole first segment indistinguishable from one that
had simply not been asked yet, and the only sentence the session produced named
the muxer and the source, which are the two subsystems that were fine.

- AudioBridge counts each arm (fed, rejected, decoded, dropped before the FIFO,
  enqueued, emitted, decode/encode errors) and keeps the decoder's own error
  code instead of discarding it. One loud line the moment enough source has gone
  in for the silence to be structural rather than start-up latency.
- The deferred first cut prints the bridge's account on the bridged path instead
  of announcing a prime scan that `scanForAudioMoovPrimeFrame` does not run there
  (it is scoped to stream-copy audio by design): the line described an action
  that never happened, on the one path where the interesting question had no line
  at all.
- Zero decoded frames ends the session at once. A restart rebuilds the muxer and
  re-opens the encoder, both downstream of a failing decoder, so the budget bought
  the same answer three times (the reporter measured 12 to 23 ms per attempt with
  identical packet counts). Frames decoded with nothing emitted is the encoder
  side, which a rebuild does heal, and keeps its revive.
- New `PlaybackErrorKind.audioBridgeProducedNoOutput`. This used to arrive as
  `.vodSourceFailed`, which reads as "the source is gone" and ends a host's
  fallback ladder; the source is neither gone nor unreadable, and a second player
  that decodes the track itself plays the file. A ladder should demote on it.

Verified end to end on a fixture that reproduces the reported log 1:1 (mono MP3
44.1 kHz MKV with its audio payloads made undecodable): before, three revive
attempts and "Source audio cannot be muxed" with no mention of audio decoding
anywhere; after, the arm is named at the deferred cut and the session ends at
once on "Audio track could not be decoded". Healthy bridged playback is
byte-identical (same fixture undamaged: ec-3 track, no new lines).

1936 swift-testing + 547 XCTest green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E5UztmCZtNYPQpzdmSdmWG
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant